﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;

namespace InSTEDD.Labs.Geo
{
	public class LatLong
	{
		public static implicit operator PPoint(LatLong latLong)
		{
			if (latLong == null)
				throw new ArgumentNullException("latLong");

			//point.X = pos.Long
			//point.Y = pos.Lat
			return new PPoint(latLong.Long, latLong.Lat);
		}

		// TODO: Ride of this ctor and use the Parse(...) methods.
		public LatLong(String latLong)
		{
			String[] parts = latLong.Split(' ');
			if (parts.Length != 2)
			{
				throw new ArgumentException(
					String.Format("Invalid LatLong specified: {0}", latLong), 
					latLong);
			}
			this.latitude = Double.Parse(parts[0]);
			this.longitude = Double.Parse(parts[1]);

			CheckLatLongIsInRange(this.latitude, this.longitude);

			this.alt = 0;
		}

		public LatLong(double lat, double longitude)
			: this(lat, longitude, 0)
		{ }

		public LatLong(double latitude, double longitude, double alt)
		{
			this.latitude = latitude;
			this.longitude = longitude;
			CheckLatLongIsInRange(this.latitude, this.longitude);
		}

		void CheckLatLongIsInRange(double latitude, double longitude)
		{
			if (latitude < -90 || latitude > 90)
			{
				throw new ArgumentOutOfRangeException("latitude");
			}

			if (longitude < -180 || latitude > 180)
			{
				throw new ArgumentOutOfRangeException("longitude");
			}
		}

		private double latitude;

		public double Lat
		{
			get { return latitude; }
			set
			{
				CheckLatLongIsInRange(value, this.longitude);
				this.latitude = value;
			}
		}

		private double longitude;

		public double Long
		{
			get { return longitude; }
			set
			{
				CheckLatLongIsInRange(this.latitude, value);
				this.longitude = value;
			}
		}

		private double alt;

		public double Alt
		{
			get { return alt; }
		}

		public override bool Equals(object obj)
		{
			LatLong anotherLatLong = obj as LatLong;
			if (anotherLatLong != null)
			{
				return this.latitude == anotherLatLong.latitude && this.longitude == anotherLatLong.longitude;
			}

			return false;
		}

		public override int GetHashCode()
		{
			return this.latitude.GetHashCode() + this.longitude.GetHashCode();
		}

		void DecimalToDMS(double dec, out double degrees, out double minutes, out double seconds)
		{
			int sign = 1;

			if (dec < 0)
			{
				sign = -1;
			}
			dec = Math.Abs(Math.Round(dec * 1000000));
			degrees = Math.Floor(dec / 1000000) * sign;
			minutes = Math.Floor(((dec / 1000000) - Math.Floor(dec / 1000000)) * 60);
			seconds = (Math.Floor(((((dec / 1000000) - Math.Floor(dec / 1000000)) * 60) - Math.Floor(((dec / 1000000) - Math.Floor(dec / 1000000)) * 60)) * 100000) * 60 / 100000);
		}

		public String ToString(String format)
		{
			format = format.ToLower();
			if (format == "dms")
			{
				double degreesLat, minutesLat, secondsLat;
				double degreesLong, minutesLong, secondsLong;
				DecimalToDMS(latitude, out degreesLat, out minutesLat, out secondsLat);
				DecimalToDMS(longitude, out degreesLong, out minutesLong, out secondsLong);
				return String.Format("Lat: {0}\xB0 {1}' {2}'' Long: {3}\xB0 {4}' {5}''", degreesLat, minutesLat, secondsLat, degreesLong, minutesLong, secondsLong);
			}
			else if (format == "decimal")
			{
				return String.Format("Lat: {0} Long: {1}", latitude, longitude);
			}

			return ToString();
		}

		public override string ToString()
		{
			return String.Format("{0}{1}{2}", latitude, LatLongDefaultSeparator, longitude);
		}

		const char LatLongDefaultSeparator = ' ';

		public static LatLong Parse(string latLongString)
		{
			return Parse(latLongString, LatLongDefaultSeparator);
		}

		public static LatLong Parse(string latLongString, char latLongSeparator)
		{
			String[] parts = latLongString.Split(new char[] { latLongSeparator }, StringSplitOptions.RemoveEmptyEntries);
			if (parts.Length != 2)
			{
				return null;
			}
			return Parse(parts[0].Trim(), parts[1].Trim());
		}

		public static LatLong Parse(string latitude, string longitude)
		{
			LatLong latLong = new LatLong(ParseLatLongComponent(latitude), ParseLatLongComponent(longitude));
			return latLong;
		}

		/// <summary>
		/// Parses a latitude or longitude value.
		/// The string value could be in both DMS and Decimal formats.
		/// </summary>
		/// <param name="component"></param>
		/// <returns></returns>
		private static double ParseLatLongComponent(string component)
		{
			// Switch , to . in case the user is in another locale
			component = component.Replace(',', '.');
			string[] componentParts = component.Split('.');

			if (componentParts.Length > 2)
			{
				// DMS component value

				// TODO: Check how the sign will be included in the compoenent string

				double sign = ParseLatLongComponent(componentParts[0]) >= 0 ? 1 : -1;
				double degrees = Math.Abs(Math.Round(ParseLatLongComponent(componentParts[0]) * 1000000d));
				double minutes = Math.Abs(Math.Round(ParseLatLongComponent(componentParts[1]) * 1000000d));
				double seconds = double.MinValue;
				if (componentParts.Length == 3)
				{
					seconds = Math.Abs(Math.Round(ParseLatLongComponent(componentParts[2]) * 1000000d));
				}
				else
				{
					seconds = Math.Abs(Math.Round(ParseLatLongComponent(string.Join(".", new string[] { componentParts[2], componentParts[3] })) * 1000000d));
				}

				return Math.Round(degrees + (minutes / 60d) + (seconds / 3600d)) * sign / 1000000d;
			}
			else
			{
				// Decimal component value
                return double.Parse(component, NumberStyles.Number, CultureInfo.InvariantCulture);
			}
		}

		public static bool TryParse(string latLongString, char latLongSeparator, out LatLong latLong)
		{
			latLong = null;

			try
			{
				latLong = Parse(latLongString, latLongSeparator);
			}
			catch
			{
				return false;
			}

			return latLong != null;
		}

		public static bool TryParse(string latLongString, out LatLong latLong)
		{
			return TryParse(latLongString, LatLongDefaultSeparator, out latLong);
		}

        /// <summary>
        /// Source http://www.meridianworlddata.com/Distance-Calculation.asp
        /// </summary>
        /// <param name="loc1"></param>
        /// <param name="loc2"></param>
        /// <returns></returns>
        public static double DistanceInKm(LatLong loc1, LatLong loc2)
        {
            if (loc1 == null || loc2 == null)
                return double.NaN;

            double lat1 = loc1.Lat;
            double lon1 = loc1.Long;
            double lat2 = loc2.Lat;
            double lon2 = loc2.Long;

            double deg_rad = 180.0 / Math.PI;
            double dist = 6378.7 * Math.Acos(Math.Sin(lat1 / deg_rad) * Math.Sin(lat2 / deg_rad) + Math.Cos(lat1 / deg_rad) * Math.Cos(lat2 / deg_rad) * Math.Cos(lon2 / deg_rad - lon1 / deg_rad));
            return dist;
        }
	}
}
